using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using HtmlAgilityPack;
using System.Net;
using System.IO;
using System.Threading.Tasks;
using System.IO.Compression;
using System.Globalization;

namespace Locator.LcboServices.Common.Services
{
    public abstract class PageRetrieverBase : IPageRetriever
    {
        private readonly static Dictionary<string, Encoding> encodingDict = BuildEncodingDictionary();

        public abstract bool CanRetrieveHtmlFor(Type lcboEntityType);

        public bool CanRetrieveHtmlFor<TLcboEntity>()
        {
            return CanRetrieveHtmlFor(typeof(TLcboEntity));
        }

        /// <summary>
        /// Gets the encodings dictionary, keyed on the charset values returned by web requests.
        /// </summary>
        /// <value>The encodings.</value>
        protected static Dictionary<string, Encoding> Encodings
        {
            get { return encodingDict; }
        }

        /// <summary>
        /// Builds the encoding dictionary.
        /// </summary>
        /// <returns></returns>
        private static Dictionary<string, Encoding> BuildEncodingDictionary()
        {
            Dictionary<string, Encoding> result = new Dictionary<string, Encoding>();
            foreach (EncodingInfo encodingInfo in Encoding.GetEncodings())
            {
                if (!result.ContainsKey(encodingInfo.Name.ToUpperInvariant()))
                {
                    result.Add(encodingInfo.Name.ToUpperInvariant(), encodingInfo.GetEncoding());
                }
            }

            return result;
        }

        /// <summary>
        /// Builds the URL.
        /// </summary>
        /// <param name="parameter">The parameter.</param>
        /// <returns>The URL with the parameter in the right spot</returns>
        protected abstract Uri BuildUrl(string parameter);

        //TODO: add a method that takes a list of parameters and get each page async

        /// <summary>
        /// Retrieves the page HTML asynchronously and returns it in a Task with an HtmlDocument.
        /// </summary>
        /// <param name="parameter">The parameter to pass to the web page.</param>
        /// <returns>A Task with an HtmlDocument</returns>
        public virtual Task<HtmlDocument> RetrievePageHtmlAsync(string parameter)
        {
            TaskCompletionSource<HtmlDocument> pageRetrievalTaskCompletionSource = new TaskCompletionSource<HtmlDocument>();
            Uri productUri = this.BuildUrl(parameter);
            HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(productUri);
            request.Method = WebRequestMethods.Http.Get;
            request.Accept = "text/html";
            request.Headers.Add("Accept-Encoding", "gzip,deflate");

            try
            {
                //Trace.TraceInformation("Beginning async request for uri {0}", request.RequestUri);
                //Stopwatch pageRetrieverStopWatch = new Stopwatch();
                //pageRetrieverStopWatch.Start();
                IAsyncResult responseResult = request.BeginGetResponse(iar =>
                {
                    HttpWebResponse myWebResponse = null;
                    HtmlDocument doc = new HtmlDocument();
                    //Trace.TraceInformation("Beginning callback for uri {0}", request.RequestUri);
                    try
                    {
                        using (myWebResponse = (HttpWebResponse)request.EndGetResponse(iar))
                        {
                            using (Stream myWebSource = GetStreamForResponse(myWebResponse))
                            {
                                string codePage = GetCodePage(myWebResponse.ContentType);
                                Encoding encoding = Encodings[codePage];
                                //Trace.TraceInformation("Beginning doc.Load for uri {0}", request.RequestUri);
                                doc.Load(myWebSource, encoding);
                                //pageRetrieverStopWatch.Stop();
                                //Trace.TraceInformation("Finished doc.Load for uri {0} after {1} milliseonds", request.RequestUri, pageRetrieverStopWatch.ElapsedMilliseconds);
                            }

                            pageRetrievalTaskCompletionSource.SetResult(doc);
                        }
                    }
                    catch (Exception exc)
                    {
                        //Trace.TraceError("Exception in RetrievePageHtmlAsync: {0}", exc.ToString());
                        pageRetrievalTaskCompletionSource.SetException(exc);
                    }
                }, null);

                if (responseResult.CompletedSynchronously)
                {
                    int ijk = 1;
                }
            }
            catch (Exception exc)
            {
                //Trace.TraceError("Exception in RetrievePageHtmlAsync: {0}", exc.ToString());
                pageRetrievalTaskCompletionSource.SetException(exc);
            }

            //Trace.TraceInformation("Ending async request phase for uri {0}", request.RequestUri);
            return pageRetrievalTaskCompletionSource.Task;
        }

        /// <summary>
        /// Parses the code page.
        /// </summary>
        /// <param name="contentType">Type of the content.</param>
        /// <returns></returns>
        protected string GetCodePage(string contentType)
        {
            string result = "utf-8";
            string[] contentTypeStrings = contentType.Split(new char[] { ';' });
            foreach (string contentTypeString in contentTypeStrings)
            {
                if (contentTypeString.Trim().StartsWith("charset", StringComparison.OrdinalIgnoreCase))
                {
                    string[] charsetStrings = contentTypeString.Split(new char[] { '=' });
                    result = charsetStrings[1];
                }
            }

            return result.ToUpperInvariant();
        }

        /// <summary>
        /// Retrieves the page HTMLS async.
        /// </summary>
        /// <param name="parameters">The parameters.</param>
        /// <param name="callback">The callback.</param>
        public virtual void RetrievePageHtmlsAsync(int[] parameters, Action<Task<HtmlDocument>[]> callback)
        {
            //var ce = new CountdownEvent(urls.Length);
            //var results = new ConcurrentQueue<Tuple<string, HtmlDocument>>();
            //Action<string, HtmlDocument> callbacki = (url, doc) =>
            //{
            //    results.Enqueue(Tuple.Create<string, HtmlDocument>(url, doc));
            //    if (ce.Signal()) callback(results);
            //};
            //   foreach(var url in urls) ValidateUrlAsync(url, callback);
            //var t = from parameter in parameters select RetrievePageHtmlAsync(parameter.ToString(CultureInfo.InvariantCulture));
            Task<HtmlDocument>[] myTasks = (from parameter in parameters select RetrievePageHtmlAsync(parameter.ToString(CultureInfo.InvariantCulture))).ToArray();
            Action<Task<HtmlDocument>[]> completedAction = (completedTasks) => { callback(completedTasks); };

            //Trace.TraceInformation("About to begin ContinueWithAll");
            Task.Factory.ContinueWhenAll<HtmlDocument>(myTasks, completedAction);
            //Trace.TraceInformation("Finished ContinueWithAll");
        }

        /// <summary>
        /// Gets the stream for response.
        /// </summary>
        /// <param name="response">The response.</param>
        /// <returns></returns>
        protected static Stream GetStreamForResponse(HttpWebResponse response)
        {
            Stream stream;
            switch (response.ContentEncoding.ToUpperInvariant())
            {
                case "GZIP":
                    stream = new GZipStream(response.GetResponseStream(), CompressionMode.Decompress);
                    break;
                case "DEFLATE":
                    stream = new DeflateStream(response.GetResponseStream(), CompressionMode.Decompress);
                    break;

                default:
                    stream = response.GetResponseStream();
                    break;
            }
            return stream;

        }

        /// <summary>
        /// Retrieves the page string async.
        /// </summary>
        /// <param name="parameter">The parameter.</param>
        /// <returns></returns>
        public virtual Task<string> RetrievePageStringAsync(string parameter)
        {
            TaskCompletionSource<string> pageRetrievalTaskCompletionSource = new TaskCompletionSource<string>();
            Uri productUri = this.BuildUrl(parameter);
            HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(productUri);
            request.Method = WebRequestMethods.Http.Get;
            request.Accept = "text/html";
            request.Headers.Add("Accept-Encoding", "gzip,deflate");

            //var request = (HttpWebRequest)WebRequest.Create(parameter);
            try
            {
                //Trace.WriteLine(String.Format(CultureInfo.CurrentCulture, "{1} Beginning async request for uri {0}", request.RequestUri, DateTime.Now.ToString("MM/dd/yyyy hh:mm:ss.fff tt", CultureInfo.CurrentCulture)));
                request.BeginGetResponse(iar =>
                {
                    HttpWebResponse myWebResponse = null;
                    string doc = null;
                    //Trace.WriteLine(String.Format(CultureInfo.CurrentCulture, "{1} Beginning callback for uri {0}", request.RequestUri, DateTime.Now.ToString("MM/dd/yyyy hh:mm:ss.fff tt", CultureInfo.CurrentCulture)));
                    try
                    {
                        using (myWebResponse = (HttpWebResponse)request.EndGetResponse(iar))
                        {
                            using (Stream myWebSource = GetStreamForResponse(myWebResponse))
                            {
                                //Trace.WriteLine(String.Format(CultureInfo.CurrentCulture, "{1} Beginning doc.Load for uri {0}", request.RequestUri, DateTime.Now.ToString("MM/dd/yyyy hh:mm:ss.fff tt", CultureInfo.CurrentCulture)));
                                byte[] docBytes = new byte[myWebSource.Length - 1];
                                myWebSource.Read(docBytes, 0, docBytes.Length);
                                string codePage = GetCodePage(myWebResponse.ContentType);
                                Encoding encoding = Encodings[codePage];
                                doc = encoding.GetString(docBytes);
                                //Trace.WriteLine(String.Format(CultureInfo.CurrentCulture, "{1} Finished doc.Load for uri {0}", request.RequestUri, DateTime.Now.ToString("MM/dd/yyyy hh:mm:ss.fff tt", CultureInfo.CurrentCulture)));
                            }
                            //using (StreamReader myWebSource = new StreamReader(myWebResponse.GetResponseStream()))
                            //{
                            //    Trace.WriteLine(String.Format(CultureInfo.CurrentCulture, "{1} Beginning doc.Load for uri {0}", request.RequestUri, DateTime.Now.ToString("MM/dd/yyyy hh:mm:ss.fff tt", CultureInfo.CurrentCulture)));
                            //    doc = myWebSource.ReadToEnd();
                            //    Trace.WriteLine(String.Format(CultureInfo.CurrentCulture, "{1} Finished doc.Load for uri {0}", request.RequestUri, DateTime.Now.ToString("MM/dd/yyyy hh:mm:ss.fff tt", CultureInfo.CurrentCulture)));
                            //}

                            pageRetrievalTaskCompletionSource.SetResult(doc);
                        }
                    }
                    catch (Exception exc)
                    {
                        pageRetrievalTaskCompletionSource.SetException(exc);
                    }
                }, null);
            }
            catch (Exception exc)
            {
                pageRetrievalTaskCompletionSource.SetException(exc);
            }

            //Trace.WriteLine(String.Format(CultureInfo.CurrentCulture, "{1} Ending async request phase for uri {0}", request.RequestUri, DateTime.Now.ToString("MM/dd/yyyy hh:mm:ss.fff tt", CultureInfo.CurrentCulture)));
            return pageRetrievalTaskCompletionSource.Task;
        }

        /// <summary>
        /// Retrieves the page strings async.
        /// </summary>
        /// <param name="parameters">The parameters.</param>
        /// <param name="callback">The callback.</param>
        public virtual void RetrievePageStringsAsync(int[] parameters, Action<Task<string>[]> callback)
        {
            //var ce = new CountdownEvent(urls.Length);
            //var results = new ConcurrentQueue<Tuple<string, HtmlDocument>>();
            //Action<string, HtmlDocument> callbacki = (url, doc) =>
            //{
            //    results.Enqueue(Tuple.Create<string, HtmlDocument>(url, doc));
            //    if (ce.Signal()) callback(results);
            //};
            //   foreach(var url in urls) ValidateUrlAsync(url, callback);
            //var t = from parameter in parameters select RetrievePageHtmlAsync(parameter.ToString(CultureInfo.InvariantCulture));
            Task<string>[] myTasks = (from parameter in parameters select RetrievePageStringAsync(parameter.ToString(CultureInfo.InvariantCulture))).ToArray();
            Action<Task<string>[]> completedAction = (completedTasks) => { callback(completedTasks); };

            //Trace.WriteLine(String.Format(CultureInfo.CurrentCulture, "{0} About to begin ContinueWithAll", DateTime.Now.ToString("MM/dd/yyyy hh:mm:ss.fff tt", CultureInfo.CurrentCulture)));
            Task.Factory.ContinueWhenAll<string>(myTasks, completedAction);
            //Trace.WriteLine(String.Format(CultureInfo.CurrentCulture, "{0} Finished ContinueWithAll", DateTime.Now.ToString("MM/dd/yyyy hh:mm:ss.fff tt", CultureInfo.CurrentCulture)));
        }

        /// <summary>
        /// Retrieves the page in an HtmlDocument.
        /// </summary>
        /// <param name="parameter">The parameter.</param>
        /// <returns>The page content in an HtmlDocument object</returns>
        public virtual HtmlDocument RetrievePageHtml(string parameter)
        {
            HtmlDocument doc = new HtmlDocument();
            Uri productUri = this.BuildUrl(parameter); //new Uri(String.Format("http://www.lcbo.com/lcbo-ear/lcbo/product/inventory/searchResults.do?language=EN&itemNumber={0}", productNumber));
            HttpWebRequest myWebRequest = (HttpWebRequest)HttpWebRequest.Create(productUri);
            myWebRequest.Method = WebRequestMethods.Http.Get;
            myWebRequest.Accept = "text/html";
            myWebRequest.Headers.Add("Accept-Encoding", "gzip,deflate");
            //make request for web page
            using (HttpWebResponse myWebResponse = (HttpWebResponse)myWebRequest.GetResponse())
            {
                using (Stream myWebSource = GetStreamForResponse(myWebResponse))//new StreamReader(myWebResponse.GetResponseStream()))
                {
                    string codePage = GetCodePage(myWebResponse.ContentType);
                    Encoding encoding = Encodings[codePage];
                    doc.Load(myWebSource, encoding);
                }
            }

            return doc;
        }
    }
}
